# Copyright (c) HySoP 2011-2024
#
# This file is part of HySoP software.
# See "https://particle_methods.gricad-pages.univ-grenoble-alpes.fr/hysop-doc/"
# for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
@file memory_reordering.py
Memory ordering related operators.
"""
from hysop.constants import Implementation, Backend, MemoryOrdering
from hysop.tools.htypes import check_instance, to_tuple, first_not_None
from hysop.tools.decorators import debug
from hysop.fields.continuous_field import Field, ScalarField
from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
from hysop.core.graph.node_generator import ComputationalGraphNodeGenerator
from hysop.core.graph.computational_operator import ComputationalGraphNode
[docs]
class MemoryReorderingNotImplementedError(NotImplementedError):
"""
Error raised for unimplemented memory reordering operator configurations.
"""
pass
[docs]
class MemoryReordering(ComputationalGraphNodeGenerator):
"""
Operator generator for inplace and out of place field memory reordering.
Available implementations are:
*python (numpy based memory reordering, n-dimensional)
This is currently used to convert C_CONTIGUOUS fields to F_CONTIGUOUS fields.
Implementations handle only one field at a time but this
graph node generator generates an operator per supplied field,
possibly on different implementation backends.
See hysop.operator.base.reorder.MemoryReorderingBase for operator backend
implementation interface.
"""
[docs]
@classmethod
def implementations(cls):
from hysop.backend.host.python.operator.memory_reordering import (
PythonMemoryReordering,
)
from hysop.backend.device.opencl.operator.memory_reordering import (
OpenClMemoryReordering,
)
_implementations = {
Implementation.PYTHON: PythonMemoryReordering,
Implementation.OPENCL: OpenClMemoryReordering,
}
return _implementations
[docs]
@classmethod
def default_implementation(cls):
msg = "MemoryReordering has no default implementation, "
msg += "implementation should match the discrete field topology backend."
raise RuntimeError(msg)
@debug
def __new__(
cls,
fields,
variables,
target_memory_order,
output_fields=None,
implementation=None,
name=None,
base_kwds=None,
**kwds,
):
base_kwds = first_not_None(base_kwds, {})
return super().__new__(
cls,
name=name,
candidate_input_tensors=None,
candidate_output_tensors=None,
**base_kwds,
)
@debug
def __init__(
self,
fields,
variables,
target_memory_order,
output_fields=None,
implementation=None,
name=None,
base_kwds=None,
**kwds,
):
"""
Initialize a MemoryReordering operator generator operating on CartesianTopology topologies.
MemoryReordering is deduced from topology requirements.
Parameters
----------
fields: Field, list or tuple of Fields
Input continuous fields to be memory reordered, at least 2D.
All fields should have the same dimension.
output_fields: Field, list or tuple of Fields, optional
Output continuous fields where the results are stored.
Reordered shapes should match the input shapes.
By default output_fields are the same as input_fields
resulting in inplace memory reordering.
Input and output are matched by order int list/tuple.
variables: dict
Dictionary of fields as keys and CartesianTopologyDescriptors as values.
target_memory_order: MemoryOrdering
Target memory order to achieve.
implementation: Implementation, optional, defaults to None
target implementation, should be contained in available_implementations().
If implementation is set and topology does not match backend,
RuntimeError will be raised on _generate.
If None, implementation will be set according to topologies backend,
different implementations may be choosen for different Fields if
defined on different backends.
name: string
prefix for generated operator names
base_kwds: dict, optional, defaults to None
Base class keywords arguments.
If None, an empty dict will be passed.
kwds:
Keywords arguments that will be passed towards implementation
memory reordering operator __init__.
Notes
-----
Out of place reordering will always be faster to process.
In place reordering requires an extra buffer and an extra copy.
* About dimensions:
- No limit.
A MemoryReordering operator implementation should support the MemoryReorderingBase
interface (see hysop.operator.base.memory_ordering.MemoryOrdering).
This ComputationalGraphNodeFrontend will generate a operator for each
input and output ScalarField pair.
All implementations should raise MemoryReorderingNotImplementedError if the user supplied
parameters leads to unimplemented or unsupported memory reordering features.
"""
input_fields = to_tuple(fields)
assert None not in input_fields
if output_fields is not None:
output_fields = to_tuple(output_fields)
output_fields = tuple(
ofield if (ofield is not None) else ifield
for (ifield, ofield) in zip(input_fields, output_fields)
)
else:
output_fields = tuple(ifield for ifield in input_fields)
check_instance(input_fields, tuple, values=Field)
check_instance(output_fields, tuple, values=Field, size=len(input_fields))
check_instance(target_memory_order, MemoryOrdering)
assert target_memory_order in (
MemoryOrdering.C_CONTIGUOUS,
MemoryOrdering.F_CONTIGUOUS,
)
candidate_input_tensors = tuple(filter(lambda x: x.is_tensor, input_fields))
candidate_output_tensors = tuple(filter(lambda x: x.is_tensor, output_fields))
base_kwds = first_not_None(base_kwds, {})
if not "mpi_params" in base_kwds:
mpi_params = next(iter(variables.values())).mpi_params
assert all([_.mpi_params == mpi_params for _ in variables.values()])
kwds.update({"mpi_params": mpi_params})
super().__init__(
name=name,
candidate_input_tensors=candidate_input_tensors,
candidate_output_tensors=candidate_output_tensors,
**base_kwds,
)
# expand tensors
ifields, ofields = (), ()
for ifield, ofield in zip(input_fields, output_fields):
msg = "Input and output field shape mismatch, got field {} of shape {} "
msg += "and field {} of shape {}."
if ifield.is_tensor ^ ofield.is_tensor:
if ifield.is_tensor:
msg = msg.format(
ifield.short_description(),
ifield.shape,
ofield.short_description(),
"(1,)",
)
else:
msg = msg.format(
ifield.short_description(),
"(1,)",
ofield.short_description(),
ofield.shape,
)
raise RuntimeError(msg)
if ifield.is_tensor and ofield.is_tensor and (ifield.shape != ofield.shape):
msg = msg.format(
ifield.short_description(),
ifield.shape,
ofield.short_description(),
ofield.shape,
)
raise RuntimeError(msg)
ifields += ifield.fields
ofields += ofield.fields
input_fields = ifields
output_fields = ofields
check_instance(input_fields, tuple, values=ScalarField)
check_instance(output_fields, tuple, values=ScalarField, size=len(input_fields))
check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors)
check_instance(base_kwds, dict, keys=str)
check_instance(name, str, allow_none=True)
fields = set(input_fields).union(output_fields)
vfields = {f for tfield in variables.keys() for f in tfield.fields}
if not fields:
raise ValueError("fields are empty.")
if not vfields:
raise ValueError("variables are empty.")
if fields != vfields:
if fields.difference(vfields):
missing = tuple(field.name for field in fields - vfields)
msg = "Missing fields in variables parameter: {}"
msg = msg.format(", ".join(missing))
else:
missing = tuple(field.name for field in vfields - fields)
msg = "Too many fields present in variables keys: {}"
msg = msg.format(", ".join(missing))
raise ValueError(msg)
dim = input_fields[0].domain.dim
if dim < 2:
msg = "input_field dimension should be at least 2 but {} is a {}D field."
msg = msg.format(input_fields[0].name, dim)
raise ValueError(msg)
for input_field, output_field in zip(input_fields, output_fields):
in_topo_descriptor = ComputationalGraphNode.get_topo_descriptor(
variables, input_field
)
out_topo_descriptor = ComputationalGraphNode.get_topo_descriptor(
variables, output_field
)
if input_field.domain != output_field.domain:
msg = "input_field {} and output_field {} do not share the same domain."
msg.format(input_field.name, output_field.name)
raise ValueError(msg)
idim = input_field.domain.dim
if idim != dim:
msg = "input_field {} is of dimension {} and does not match first "
msg += "input_field {} dimension {}."
msg.format(input_field, idim, input_fields[0].name, dim)
raise ValueError(msg)
self.input_fields = input_fields
self.output_fields = output_fields
self.variables = variables
self.implementation = implementation
self.target_memory_order = target_memory_order
self.kwds = kwds
def _get_op_and_check_implementation(self, src_topo, dst_topo):
backend = getattr(src_topo, "backend", None) or getattr(
dst_topo, "backend", None
)
implementation = self.implementation
if backend is None:
if implementation is None:
msg = (
"Source and destination topology descriptors do not expose backend "
)
msg += (
"attribute and self.implementation was not set, cannot determine "
)
msg += "the target memory reordering operator."
raise ValueError(msg)
if implementation not in self.implementations():
msg = f"Unknown memory reordering implementation {implementation}."
raise ValueError(msg)
op_cls = self.implementations()[implementation]
else:
def check_impl(impl, allowed):
if impl not in allowed:
msg = "Specified memory reordering implementation {} but this is not a valid "
msg += "implementation for fields defined on backend of kind {}."
msg = msg.format(impl, backend.kind)
raise ValueError(msg)
if backend.kind == Backend.HOST:
implementation = first_not_None(
self.implementation, Implementation.PYTHON
)
check_impl(implementation, (Implementation.PYTHON,))
op_cls = self.implementations()[implementation]
elif backend.kind == Backend.OPENCL:
implementation = first_not_None(
self.implementation, Implementation.OPENCL
)
check_impl(implementation, (Implementation.OPENCL,))
op_cls = self.implementations()[implementation]
else:
msg = f"Unsupported memory reordering backend {backend.kind}."
raise ValueError(msg)
from hysop.operator.base.memory_reordering import MemoryReorderingBase
if not issubclass(op_cls, MemoryReorderingBase):
msg = (
"Class {} does not inherit from the memory ordering operator interface "
)
msg += "({}). This is an implementation error."
msg = msg.format(op_cls, MemoryReorderingBase)
raise TypeError(msg)
if not issubclass(op_cls, ComputationalGraphNode):
msg = (
"Class {} does not inherit from the computational graph node interface"
)
msg += "({}). This is an implementation error."
msg = msg.format(op_cls, ComputationalGraphNode)
raise TypeError(msg)
return op_cls
@debug
def _generate(self):
nodes = []
for ifield, ofield in zip(self.input_fields, self.output_fields):
src_topo = ComputationalGraphNode.get_topo_descriptor(
self.variables, ifield
)
dst_topo = ComputationalGraphNode.get_topo_descriptor(
self.variables, ofield
)
MemoryReorderingOp = self._get_op_and_check_implementation(
src_topo, dst_topo
)
kwds = self.kwds.copy()
variables = {ifield: src_topo}
variables[ofield] = dst_topo
# instantiate operator
node = MemoryReorderingOp(
input_field=ifield,
output_field=ofield,
variables=variables,
target_memory_order=self.target_memory_order,
**kwds,
)
nodes.append(node)
if not nodes:
msg = "No memory reordering needed for given input and output fields."
raise RuntimeError(msg)
return nodes